#include <string.h>
#include <unordered_map>
#include <queue>
#include <netdb.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sstream>
#include <sys/time.h>
#include <vector>

#include "packet.h"

using namespace std;

#define MAX_HOSTNAME_SIZE 128
#define TIME_BETWEEN_CHECKS 50
#define NUM_EXPECTED_ARGS 5
#define MILLI_TO_WAIT 50
#define MAX_PACKET_SIZE 2048
#define RETURN_TTL_VALUE 999991

typedef struct routePoint {
	unsigned long lastSeq;
	string ipAddr;
	int isUp;
	vector<routePoint*> possibleRoutes;	
} routePoint;

typedef struct destinations {
	int dist;
	string next_hop; // IP,Port
} destinations;

unordered_map<string, destinations> dist_to_nodes; // node, dist to that node, next hop
unordered_map<string, routePoint> myHash;
int sockfd;

string my_ip_dot;
unsigned long my_ip;
int port;
routePoint* me;

vector<string> dests;

//Function Declarations
string int_to_string(int);
void forwardPacket(Packet*);
void handleLinkRequest(Packet*);
void handleUpdate(Packet*);
string long_to_dotted_ip(unsigned long);
string unsigned_short_to_string(unsigned short);
string long_to_string(long);
void print_all_distances();

int main(int argc, char **argv){
	port = -1;
	string topologyFile = "";
	unsigned long mySeqNum = 0;

	//Argument Getting
	if(argc != NUM_EXPECTED_ARGS){
		cout << "Usage: emulator -p <port> -f <filename>" << endl;
		exit(-1);
	}

	for(int i = 1; i < argc; i++) {
		if(strcmp(argv[i], "-p") == 0) {
			port = atoi(argv[i+1]);
		}
		else if(strcmp(argv[i], "-f") == 0) {
			topologyFile = argv[i+1];
		}
		else {
			cout << "Usage: emulator -p <port> -f <filename>" << endl;
			exit(-1);
		}
		i++;
	}

	if(port == -1 || topologyFile.empty()){
		cout << "Usage: emulator -p <port> -f <filename>" << endl;
		exit(-1);
	}

	// Get my IP
	char my_hostname[MAX_HOSTNAME_SIZE]; 
	gethostname(my_hostname, MAX_HOSTNAME_SIZE);
	struct hostent *h = gethostbyname(my_hostname);
	my_ip_dot = inet_ntoa(*((struct in_addr *)h->h_addr_list[0]));
	my_ip = inet_addr(my_ip_dot.c_str());

	ifstream inFile(topologyFile);
	
	/*string emulator, nextHop, destination;
	int delay, emulatorPort, nextHopPort, destinationPort;
	double lossProbability;
	unordered_map<string, route> myHash;

	struct hostent* h;
	string dstIP = "";*/
	//Better way to read lines?  Don't read everything
	//If you don't have to.
	string my_hash_val = my_ip_dot + "," + int_to_string(port);
	cout << my_hash_val << endl;
	string line;
	while(getline(inFile, line)) {
		istringstream partLine(line);
		string word;
		partLine >> word;
		if(dist_to_nodes.count(word) > 0) {
			//cout << "This route was already used: " << word << endl;
			continue;
		}
		else if(myHash.count(word) > 0) {
			destinations* dest = new destinations;
			dest->next_hop = "";
			dest->dist = -1;

			dist_to_nodes[word] = *dest;
			dests.push_back(word);
			routePoint* tmp = &myHash[word];

			while(partLine >> word) {
				if(myHash.count(word) == 0) {
					routePoint* tmp2 = new routePoint;
					tmp2->ipAddr = word;
					tmp2->isUp = 1;
					myHash[word] = *tmp2;
				}
				tmp->possibleRoutes.push_back(&myHash[word]);
			}

		}
		else if(myHash.count(word) == 0) {
			destinations* dest = new destinations;
			dest->next_hop = "";
			dest->dist = -1;

			dist_to_nodes[word] = *dest;
			dests.push_back(word);

			myHash[word] = routePoint();
			myHash[word].ipAddr = word;
			myHash[word].isUp = 1;
			string neighbor = "";
			while (partLine >> neighbor) {
				if(myHash.count(neighbor) == 0) {
					myHash[neighbor] = routePoint();
					myHash[neighbor].ipAddr = neighbor;
					myHash[neighbor].isUp = 1;
				}
				myHash[word].possibleRoutes.push_back(&myHash[neighbor]);
			}
		}
		else {
			cout << "Something went awry" << endl;
		}
	}
	// Set distances to your neighbors
	cout << "Neighbors: " << (int) myHash[my_hash_val].possibleRoutes.size() << endl;
	for (int i = 0; i < (int) myHash[my_hash_val].possibleRoutes.size(); i++) {
		string curHashVal = myHash[my_hash_val].possibleRoutes[i]->ipAddr;
		//cout << "Neighbor: " << curHashVal << endl;
		dist_to_nodes[curHashVal].next_hop = curHashVal;
		dist_to_nodes[curHashVal].dist = 1;
	}

	sockfd = socket(AF_INET, SOCK_DGRAM, 0);

	struct sockaddr_in servaddrThem, servaddrMe, servaddrRec;
	bzero(&servaddrMe, sizeof(servaddrMe));
	servaddrMe.sin_family = AF_INET;
	servaddrMe.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddrMe.sin_port = htons(port);
	
	if (bind(sockfd, (struct sockaddr*) &servaddrMe, sizeof(servaddrMe)) == -1) {
    	perror("Bind");
   	 	exit(EXIT_FAILURE);
  }
  
  //cout << "I'm listening on port: " << port << endl;

	timeval curTime;
	int currentTimeMilli;
	int lastCheckTime;
	int waitingTime;
	int startWaitingTime;

	gettimeofday(&curTime, NULL);
	lastCheckTime = curTime.tv_sec * 1000 + curTime.tv_usec / 1000;
	me = &myHash[my_ip_dot + "," + int_to_string(port)];
	while (true) {
		gettimeofday(&curTime, NULL);
		currentTimeMilli = curTime.tv_sec * 1000 + curTime.tv_usec / 1000;

		if(currentTimeMilli - lastCheckTime > TIME_BETWEEN_CHECKS) {
			//print_all_distances();
			for(int i = 0; i < (int) me->possibleRoutes.size(); i++) {
				Packet* tmp = new Packet();
				tmp->set_type(LINK_REQUEST);
				string fullAddr = me->possibleRoutes[i]->ipAddr;
				stringstream stream(fullAddr);
				string addressTo, portTo;
				getline(stream, addressTo, ',');
				getline(stream, portTo, ',');
				tmp->set_src_ip(my_ip);
				tmp->set_src_port(port);
				tmp->set_dest_ip(inet_addr(addressTo.c_str()));
				tmp->set_dest_port(atoi(portTo.c_str()));
				


				bzero(&servaddrThem, sizeof(servaddrThem));
				servaddrThem.sin_family = AF_INET;
				servaddrThem.sin_addr.s_addr = inet_addr(addressTo.c_str());
				servaddrThem.sin_port = htons(atoi(portTo.c_str()));
				
				//cout << "Sending link request to: " << fullAddr << endl;
				
				tmp->send_packet(sockfd, servaddrThem);
				
			}

			gettimeofday(&curTime, NULL);
			startWaitingTime = curTime.tv_sec * 1000 + curTime.tv_usec / 1000;
			waitingTime = startWaitingTime;
			int countRec = 0;
			
			//cout << "STARTING REQUEST WAIT" << endl;
			while(waitingTime - startWaitingTime < MILLI_TO_WAIT && countRec < (int) me->possibleRoutes.size()) {				
				Packet *thePacket = Packet::receive_packet(sockfd, &servaddrRec, MAX_PACKET_SIZE, false);
				if(thePacket && thePacket->get_type() == TRACE) {
					//cout << "RECEIVE TRACE1" << endl;
					//thePacket->print_packet();
					//cout << "end" << endl;
					if (my_ip == thePacket->get_src_ip() && 
					port == thePacket->get_src_port()) {
						//cout << "Node received its own packet, drop packet" << endl;
						dist_to_nodes[thePacket->get_dest_ip() + "," + thePacket->get_dest_port()].dist = -1;
						dist_to_nodes[thePacket->get_dest_ip() + "," + thePacket->get_dest_port()].next_hop = "";
					}
					else {
						forwardPacket(thePacket);
					}
				}
				else if(thePacket && thePacket->get_type() == LINK_REQUEST) {
					handleLinkRequest(thePacket);
				}
				else if(thePacket && thePacket->get_type() == LINK_RESPONSE) {
					unsigned long lTheirIp = thePacket->get_src_ip();
					unsigned short theirPort = thePacket->get_src_port();
					string theirIp = long_to_dotted_ip(lTheirIp);
					routePoint* them = &myHash[theirIp + "," + unsigned_short_to_string(theirPort)];
					them->isUp = 2;
					countRec++;
					
					//cout << "Who is getting set to 2: " << theirIp << "," << theirPort << " " << them->isUp << endl;
					
					//cout << "Received link response from: " << theirIp + "," + unsigned_short_to_string(theirPort) << endl;
				}
				else if(thePacket && thePacket->get_type() == LINK_UPDATE) {
					//cout << "HANDLING UPDATE 1" <<  endl;
					print_all_distances();
					handleUpdate(thePacket);
				}
				else if(thePacket) {
					cout << "bad packet" << endl;
				}
				gettimeofday(&curTime, NULL);
				waitingTime = curTime.tv_sec * 1000 + curTime.tv_usec / 1000;
			}
			//cout << "END REQUEST WAIT!" << endl;
			for(int i = 0; i < (int) me->possibleRoutes.size(); i++) {
				if(me->possibleRoutes[i]->isUp == 2 || me->possibleRoutes[i]->isUp == 1) {
					me->possibleRoutes[i]->isUp--;
					//cout << "Setting route to " << me->possibleRoutes[i]->ipAddr << " to " << me->possibleRoutes[i]->isUp << endl;
				}
			}

			string data = "";
			for(int i = 0; i < (int) me->possibleRoutes.size(); i++) {
				data += me->possibleRoutes[i]->ipAddr;
				data += "|";
				data += int_to_string(me->possibleRoutes[i]->isUp);
				data += "!";
			}

			//cout << "MY SEND" << endl;
			for(int i = 0; i < (int) me->possibleRoutes.size(); i++) {
				Packet *tmp = new Packet();
				tmp->set_seq(mySeqNum);
				mySeqNum++;
				tmp->set_nodes(data);
				tmp->set_type(LINK_UPDATE);
				string fullAddr = me->possibleRoutes[i]->ipAddr;
				stringstream stream(fullAddr);
				string addressTo, portTo;
				getline(stream, addressTo, ',');
				getline(stream, portTo, ',');
				tmp->set_src_ip(my_ip);
				tmp->set_src_port(port);
				tmp->set_dest_ip(inet_addr(addressTo.c_str()));
				tmp->set_dest_port(atoi(portTo.c_str()));
				
				forwardPacket(tmp);
				
			}
			 
			lastCheckTime = currentTimeMilli;
		}

		Packet *thePacket = Packet::receive_packet(sockfd, &servaddrRec, MAX_PACKET_SIZE, false);

		if(thePacket && thePacket->get_type() == TRACE) {
			//cout << "RECEIVE TRACE1" << endl;
			//thePacket->print_packet();
			//cout << "end" << endl;
			if (my_ip == thePacket->get_src_ip() && 
					port == thePacket->get_src_port()) {
				//cout << "Node received its own packet, drop packet" << endl;
				dist_to_nodes[thePacket->get_dest_ip() + "," + thePacket->get_dest_port()].dist = -1;
				dist_to_nodes[thePacket->get_dest_ip() + "," + thePacket->get_dest_port()].next_hop = "";
			}
			else {
				forwardPacket(thePacket);
			}
		}
		else if(thePacket && thePacket->get_type() == LINK_REQUEST) {
			handleLinkRequest(thePacket);
		}
		else if(thePacket && thePacket->get_type() == LINK_UPDATE) {
			//thePacket->print_packet();
			//cout << "HANDLING UPDATE 2" << endl;
			handleUpdate(thePacket);
		}
		else if(thePacket && thePacket->get_type() == LINK_RESPONSE) {
			cout << "bad response" << endl;
		} 
		else if(thePacket) {
			cout << "bad packet" << endl;
		}
	}

	return 0;
}

void handleUpdate(Packet* thePacket) {
	string source = long_to_dotted_ip(thePacket->get_src_ip()) + "," + unsigned_short_to_string(thePacket->get_src_port());
	
	// Ensure you're not receiving an update from yourself
	if(my_ip == thePacket->get_src_ip() &&
			port == thePacket->get_src_port()) {
		//cout << "You're the source: " << source << endl;
		return;
	}

	string hisIp = long_to_dotted_ip(thePacket->get_src_ip());
	string hisPort = unsigned_short_to_string(thePacket->get_src_port());
	routePoint* hisRoute = &myHash[hisIp + "," + hisPort];
	unsigned long hisSeq = hisRoute->lastSeq;
	if(hisSeq >= thePacket->get_seq()) {
		//cout << "This is an old update from " << source << ": " << thePacket->get_seq() << endl;
		return;
	}
	hisRoute->lastSeq = thePacket->get_seq();
	string data = thePacket->get_nodes();
	//cout << "Data received: " << data << endl;

	stringstream stream(data);
	string node;
	
	//cout << "Packet originating from: " << source << endl;
	//cout << "Current distance to " << source << ": " << dist_to_nodes[source].dist << endl;
	//cout << "Current next hop to " << source << ": " << dist_to_nodes[source].next_hop << endl;
	
	while(getline(stream, node, '!')) {
		stringstream stream2(node);
		string ipHash, state;
		getline(stream2, ipHash, '|');
		getline(stream2, state, '|');
		if(myHash[ipHash].isUp != 2){
			myHash[ipHash].isUp = atoi(state.c_str());
		}
		//cout << "Current source neighbor: " << ipHash << endl;
		
		if (ipHash == (my_ip_dot + "," + unsigned_short_to_string(port))) {
			//cout << "Don't update yourself" << endl;
			continue;
		}
		
		/*if(hash_val == "128.105.112.4,5002") {
			cout << "data: " << data << endl;
			cout << "line: " << node << endl;
			cout << "ipHash: " << ipHash << endl;
			cout << "state: " << state << endl;
		
			cout << "STATE: " << state << endl;
			cout << "hisIp,hisPort: " << hisIp + "," + hisPort << endl;
			cout << "dist_to_nodes[hisIp,hisPort].dist: " << dist_to_nodes[hisIp + "," + hisPort].dist << endl;
			cout << "dist_to_nodes[hisIp,hisPort].dist + 1: " << dist_to_nodes[hisIp + "," + hisPort].dist + 1 << endl;
			cout << "dist_to_nodes[ipHash].dist: " << dist_to_nodes[ipHash].dist << endl;
		}*/
		
		if(state == "0") {
			//cout << ipHash << " IS DOWN" << endl;
			dist_to_nodes[ipHash].dist = -1;
			dist_to_nodes[ipHash].next_hop = "";
			
			// Reset next hops equal to the downed node to nothing
			for(int i = 0; i < dests.size(); i++) {
				//cout << "Dest[" << i << "]: " << dests[i] << endl;
				if (dist_to_nodes[dests[i]].next_hop == ipHash) {
					dist_to_nodes[dests[i]].dist = -1;
					dist_to_nodes[dests[i]].next_hop = "";
					
					//cout << "Resetting next hop to: " << dist_to_nodes[dests[i]].next_hop << endl; 
					return;
				}
			}
		}
		else if(dist_to_nodes[hisIp + "," + hisPort].dist != -1 &&
						(dist_to_nodes[hisIp + "," + hisPort].dist + 1 < 
						dist_to_nodes[ipHash].dist || dist_to_nodes[ipHash].dist == -1)){
			
			string myself = my_ip_dot + "," + unsigned_short_to_string(port);
			
			for(int i = 0; i < (int) myHash[myself].possibleRoutes.size(); i++) {
				if (myHash[myself].possibleRoutes[i]->ipAddr == ipHash) {
					dist_to_nodes[ipHash].dist = 1;
					dist_to_nodes[ipHash].next_hop = ipHash;
					
					//cout << "Resetting " << ipHash << " to be your neighbor" << endl; 
					return;
				}
			}
						
			//cout << "Current distance to " << ipHash << ": " << dist_to_nodes[ipHash].dist << endl;
			//cout << "Current distance to " << ipHash << ": " << dist_to_nodes[ipHash].next_hop << endl;
						
			dist_to_nodes[ipHash].dist = dist_to_nodes[hisIp + "," + hisPort].dist + 1;
			dist_to_nodes[ipHash].next_hop = dist_to_nodes[hisIp + "," + hisPort].next_hop;
			
			//cout << "New distance to " << ipHash << ": " << dist_to_nodes[ipHash].dist << endl;
			//cout << "New distance to " << ipHash << ": " << dist_to_nodes[ipHash].next_hop << endl;
			
			//dist_to_nodes[hisIp + "," + hisPort].dist = dist_to_nodes[ipHash].dist + 1;
			//dist_to_nodes[hisIp + "," + hisPort].next_hop = dist_to_nodes[ipHash].next_hop;
			
			//cout << "New distance: " << dist_to_nodes[hisIp + "," + hisPort].dist << endl;
			//cout << "New next hop: " << dist_to_nodes[hisIp + "," + hisPort].next_hop << endl;
		}
	}
	
	//cout << "Calculating neighbors" << endl;
	print_all_distances();
	
	for(int i = 0; i < (int) me->possibleRoutes.size(); i++) {
		Packet* tmp = new Packet();
		tmp->set_type(LINK_UPDATE);
		string fullAddr = me->possibleRoutes[i]->ipAddr;
		stringstream stream(fullAddr);
		string addressTo, portTo;
		getline(stream, addressTo, ',');
		getline(stream, portTo, ',');
		tmp->set_src_ip(thePacket->get_src_ip());
		tmp->set_src_port(thePacket->get_src_port());
		tmp->set_dest_ip(inet_addr(addressTo.c_str()));
		tmp->set_dest_port(atoi(portTo.c_str()));
		tmp->set_nodes(thePacket->get_nodes());
		tmp->set_seq(thePacket->get_seq());

		forwardPacket(tmp);
	}
}

void handleLinkRequest(Packet* thePacket) {
	struct sockaddr_in servaddrThem;
	Packet* tmp = new Packet();
	tmp->set_src_ip(thePacket->get_dest_ip());
	tmp->set_dest_ip(thePacket->get_src_ip());
	tmp->set_src_port(thePacket->get_dest_port());
	tmp->set_dest_port(thePacket->get_src_port());
	tmp->set_type(LINK_RESPONSE);

	bzero(&servaddrThem, sizeof(servaddrThem));
	servaddrThem.sin_family = AF_INET;
	servaddrThem.sin_addr.s_addr = tmp->get_dest_ip();
	servaddrThem.sin_port = htons(tmp->get_dest_port());
	
	tmp->send_packet(sockfd, servaddrThem);
	
	//cout << "Received link request from: " << long_to_dotted_ip(thePacket->get_src_ip()) + "," + unsigned_short_to_string(thePacket->get_src_port()) << endl;
	//cout << "Sending response to:        " << long_to_dotted_ip(tmp->get_dest_ip()) + "," + unsigned_short_to_string(tmp->get_dest_port()) << endl;
}

void forwardPacket(Packet *thePacket) {
	struct sockaddr_in servaddrThem2;
	if(thePacket->get_ttl() == 0 && thePacket->get_type() == TRACE) {
		thePacket->set_dest_ip(thePacket->get_src_ip());
		thePacket->set_dest_port(thePacket->get_src_port());
		thePacket->set_src_ip(my_ip);
		thePacket->set_src_port(port);
		thePacket->set_ttl(RETURN_TTL_VALUE);
		
		bzero(&servaddrThem2, sizeof(servaddrThem2));
		servaddrThem2.sin_family = AF_INET;
		servaddrThem2.sin_addr.s_addr = thePacket->get_dest_ip();
		servaddrThem2.sin_port = htons(thePacket->get_dest_port());
		
		//cout << "Sending trace1" << endl;
		//thePacket->print_packet();
		//cout << "end" << endl;
		thePacket->send_packet(sockfd, servaddrThem2);
		
		
		
		return;
	}
	
	if (thePacket->get_type() == TRACE) {
		if (thePacket->get_ttl() > 0) {
			//cout << "Setting ttl from " << thePacket->get_ttl();
			thePacket->set_ttl(thePacket->get_ttl()-1);
			//cout << " to " << thePacket->get_ttl() << endl;
		}
	}
	
	string hashKey = long_to_dotted_ip(thePacket->get_dest_ip()) + "," + unsigned_short_to_string(thePacket->get_dest_port());
	string nextHop = dist_to_nodes[hashKey].next_hop;

	stringstream stream(nextHop);
	string addressTo, portTo;
	getline(stream, addressTo, ',');
	getline(stream, portTo, ',');

	bzero(&servaddrThem2, sizeof(servaddrThem2));
	servaddrThem2.sin_family = AF_INET;
	servaddrThem2.sin_addr.s_addr = inet_addr(addressTo.c_str());
	servaddrThem2.sin_port = htons(atoi(portTo.c_str()));
			
	if (thePacket->get_type() == TRACE) {
		//cout << "Sending trace2" << endl;
		//cout << "Next hop: " << nextHop << endl;
		//thePacket->print_packet();		
		//cout << "end" << endl;
	}
	/*else {
		cout << "Forwarding update to: " << hashKey << endl;
		cout << "Next hop: " << nextHop << endl;
	}*/
	
	//cout << "Data sending: " << thePacket->get_nodes() << endl;
	thePacket->send_packet(sockfd, servaddrThem2);

	//cout << "Received link update from: " << long_to_dotted_ip(thePacket->get_src_ip()) + "," + unsigned_short_to_string(thePacket->get_src_port()) << endl;
}

void print_all_distances() {
	cout << "***** NEIGHBORS *****" << endl;
	cout << "Node " <<  "128.105.112.2,5000: " << dist_to_nodes["128.105.112.2,5000"].next_hop << " : " << dist_to_nodes["128.105.112.2,5000"].dist << endl;
	cout << "Node " <<  "128.105.112.2,5001: " << dist_to_nodes["128.105.112.2,5001"].next_hop << " : " << dist_to_nodes["128.105.112.2,5001"].dist << endl;
	cout << "Node " <<  "128.105.112.2,5002: " << dist_to_nodes["128.105.112.2,5002"].next_hop << " : " << dist_to_nodes["128.105.112.2,5002"].dist << endl;
	cout << "Node " <<  "128.105.112.1,6000: " << dist_to_nodes["128.105.112.1,6000"].next_hop << " : " << dist_to_nodes["128.105.112.1,6000"].dist << endl;
	cout << "Node " <<  "128.105.112.1,6001: " << dist_to_nodes["128.105.112.1,6001"].next_hop << " : " << dist_to_nodes["128.105.112.1,6001"].dist << endl;
	/*cout << "Node " <<  "128.105.112.1,6002: " << dist_to_nodes["128.105.112.1,6002"].next_hop << " : " << dist_to_nodes["128.105.112.1,6002"].dist << endl;*/
}

string unsigned_short_to_string(unsigned short n) {
	std::ostringstream result;
  result << n;
  return result.str();
}

string int_to_string(int n) {
  std::ostringstream result;
	result << n;
  string x = result.str();
	return x;
}

string long_to_string(long n) {
  std::ostringstream result;
  result << n;
  string x = result.str();
	return x;
}

string long_to_dotted_ip(unsigned long ip) {
	struct in_addr addr;
	addr.s_addr = ip;
	char *dot_ip = inet_ntoa(addr);

	return dot_ip;
}
